/**
 * AJAX Upload ( http://valums.com/ajax-upload/ )
 * Copyright (c) Andris Valums
 * Licensed under the MIT license ( http://valums.com/mit-license/ )
 * Thanks to Gary Haran, David Mark, Corey Burns and others for contributions
 */
(function () {
	/* global window */
	/* jslint browser: true, devel: true, undef: true, nomen: true, bitwise: true, regexp: true, newcap: true, immed: true */
	/**
	 * Wrapper for FireBug's console.log
	 */
	function log(){
		if (typeof(console) != 'undefined' && typeof(console.log) == 'function'){
			Array.prototype.unshift.call(arguments, '[Ajax Upload]');
			console.log( Array.prototype.join.call(arguments, ' '));
		}
	}

	/**
	 * Attaches event to a dom element.
	 * @param {Element} el
	 * @param type event name
	 * @param fn callback This refers to the passed element
	 */
	function addEvent(el, type, fn){
		if (el.addEventListener) {
			el.addEventListener(type, fn, false);
		} else if (el.attachEvent) {
			el.attachEvent('on' + type, function(){
				fn.call(el);
			});
		} else {
			throw new Error('not supported or DOM not loaded');
		}
	}

	/**
	 * Attaches resize event to a window, limiting
	 * number of event fired. Fires only when encounteres
	 * delay of 100 after series of events.
	 *
	 * Some browsers fire event multiple times when resizing
	 * http://www.quirksmode.org/dom/events/resize.html
	 *
	 * @param fn callback This refers to the passed element
	 */
	function addResizeEvent(fn){
		var timeout;
		addEvent(window, 'resize', function(){
			if (timeout){
				clearTimeout(timeout);
			}
			timeout = setTimeout(fn, 100);
		});
	}
	// Needs more testing, will be rewriten for next version
	// getOffset function copied from jQuery lib (http://jquery.com/)
	if (document.documentElement.getBoundingClientRect){
		// Get Offset using getBoundingClientRect
		// http://ejohn.org/blog/getboundingclientrect-is-awesome/
		var getOffset = function(el){
			var box = el.getBoundingClientRect();
			var doc = el.ownerDocument;
			var body = doc.body;
			var docElem = doc.documentElement; // for ie
			var clientTop = docElem.clientTop || body.clientTop || 0;
			var clientLeft = docElem.clientLeft || body.clientLeft || 0;
			// In Internet Explorer 7 getBoundingClientRect property is treated as physical,
			// while others are logical. Make all logical, like in IE8.
			var zoom = 1;
			if (body.getBoundingClientRect) {
				var bound = body.getBoundingClientRect();
				zoom = (bound.right - bound.left) / body.clientWidth;
			}
			if (zoom > 1) {
				clientTop = 0;
				clientLeft = 0;
			}
			var top = box.top / zoom + (window.pageYOffset || docElem && docElem.scrollTop / zoom || body.scrollTop / zoom) - clientTop, left = box.left / zoom + (window.pageXOffset || docElem && docElem.scrollLeft / zoom || body.scrollLeft / zoom) - clientLeft;
			return {
				top: top,
				left: left
			};
		};
	} else {
		// Get offset adding all offsets
		var getOffset = function(el){
			var top = 0, left = 0;
			do {
				top += el.offsetTop || 0;
				left += el.offsetLeft || 0;
				el = el.offsetParent;
			} while (el);
			return {
				left: left,
				top: top
			};
		};
	}
	/**
	 * Returns left, top, right and bottom properties describing the border-box,
	 * in pixels, with the top-left relative to the body
	 * @param {Element} el
	 * @return {Object} Contains left, top, right,bottom
	 */
	function getBox(el){
		var left, right, top, bottom;
		var offset = getOffset(el);
		left = offset.left;
		top = offset.top;
		right = left + el.offsetWidth;
		bottom = top + el.offsetHeight;
		return {
			left: left,
			right: right,
			top: top,
			bottom: bottom
		};
	}
	/**
	 * Helper that takes object literal
	 * and add all properties to element.style
	 * @param {Element} el
	 * @param {Object} styles
	 */
	function addStyles(el, styles){
		for (var name in styles) {
			if (styles.hasOwnProperty(name)) {
				el.style[name] = styles[name];
			}
		}
	}
	/**
	 * Function places an absolutely positioned
	 * element on top of the specified element
	 * copying position and dimentions.
	 * @param {Element} from
	 * @param {Element} to
	 */
	function copyLayout(from, to){
		var box = getBox(from);
		addStyles(to, {
			position: 'absolute',
			left : box.left + 'px',
			top : box.top + 'px',
			width : from.offsetWidth + 'px',
			height : from.offsetHeight + 'px'
		});
	}

	/**
	* Creates and returns element from html chunk
	* Uses innerHTML to create an element
	*/
	var toElement = (function(){
		var div = document.createElement('div');
		return function(html){
			div.innerHTML = html;
			var el = div.firstChild;
			return div.removeChild(el);
		};
	})();
	/**
	 * Function generates unique id
	 * @return unique id
	 */
	var getUID = (function(){
		var id = 0;
		return function(){
			return 'ValumsAjaxUpload' + id++;
		};
	})();

	/**
	 * Get file name from path
	 * @param {String} file path to file
	 * @return filename
	 */
	function fileFromPath(file){
		return file.replace(/.*(\/|\\)/, "");
	}
	/**
	 * Get file extension lowercase
	 * @param {String} file name
	 * @return file extenstion
	 */
	function getExt(file){
		return (-1 !== file.indexOf('.')) ? file.replace(/.*[.]/, '') : '';
	}

	function hasClass(el, name){
		var re = new RegExp('\\b' + name + '\\b');
		return re.test(el.className);
	}
	function addClass(el, name){
		if ( ! hasClass(el, name)){
			el.className += ' ' + name;
		}
	}
	function removeClass(el, name){
		var re = new RegExp('\\b' + name + '\\b');
		el.className = el.className.replace(re, '');
	}
	function removeNode(el){
		el.parentNode.removeChild(el);
	}

	/**
	 * Easy styling and uploading
	 * @constructor
	 * @param button An element you want convert to
	 * upload button. Tested dimentions up to 500x500px
	 * @param {Object} options See defaults below.
	 */
	window.AjaxUpload = function(button, options){
		this._settings = {
			// Location of the server-side upload script
			action: 'upload.php',
			// File upload name
			name: 'userfile',
			// Additional data to send
			data: {},
			// Submit file as soon as it's selected
			autoSubmit: true,
			// The type of data that you're expecting back from the server.
			// html and xml are detected automatically.
			// Only useful when you are using json data as a response.
			// Set to "json" in that case.
			responseType: false,
			// Class applied to button when mouse is hovered
			hoverClass: 'hover',
			// Class applied to button when AU is disabled
			disabledClass: 'disabled',
			// When user selects a file, useful with autoSubmit disabled
			// You can return false to cancel upload
			onChange: function(file, extension){
			},
			// Callback to fire before file is uploaded
			// You can return false to cancel upload
			onSubmit: function(file, extension){
			},
			// Fired when file upload is completed
			// WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE!
			onComplete: function(file, response){
			}
		};
		// Merge the users options with our defaults
		for (var i in options) {
			if (options.hasOwnProperty(i)){
				this._settings[i] = options[i];
			}
		}
		// button isn't necessary a dom element
		if (button.jquery){
			// jQuery object was passed
			button = button[0];
		} else if (typeof button == "string") {
			if (/^#.*/.test(button)){
				// If jQuery user passes #elementId don't break it
				button = button.slice(1);
			}
			button = document.getElementById(button);
		}
		if ( ! button || button.nodeType !== 1){
			throw new Error("Please make sure that you're passing a valid element");
		}
		if ( button.nodeName.toUpperCase() == 'A'){
			// disable link
			addEvent(button, 'click', function(e){
				if (e && e.preventDefault){
					e.preventDefault();
				} else if (window.event){
					window.event.returnValue = false;
				}
			});
		}
		// DOM element
		this._button = button;
		// DOM element
		this._input = null;
		// If disabled clicking on button won't do anything
		this._disabled = false;
		// if the button was disabled before refresh if will remain
		// disabled in FireFox, let's fix it
		this.enable();
		this._rerouteClicks();
	};
	// assigning methods to our class
	AjaxUpload.prototype = {
		setData: function(data){
			this._settings.data = data;
		},
		disable: function(){
			addClass(this._button, this._settings.disabledClass);
			this._disabled = true;
			var nodeName = this._button.nodeName.toUpperCase();
			if (nodeName == 'INPUT' || nodeName == 'BUTTON'){
				this._button.setAttribute('disabled', 'disabled');
			}
			// hide input
			if (this._input){
				// We use visibility instead of display to fix problem with Safari 4
				// The problem is that the value of input doesn't change if it
				// has display none when user selects a file
				this._input.parentNode.style.visibility = 'hidden';
			}
		},
		enable: function(){
			removeClass(this._button, this._settings.disabledClass);
			this._button.removeAttribute('disabled');
			this._disabled = false;
		},
		/**
		 * Creates invisible file input
		 * that will hover above the button
		 * <div><input type='file' /></div>
		 */
		_createInput: function(){
			var self = this;
			var input = document.createElement("input");
			input.setAttribute('type', 'file');
			input.setAttribute('name', this._settings.name);
			addStyles(input, {
				'position' : 'absolute',
				// in Opera only 'browse' button
				// is clickable and it is located at
				// the right side of the input
				'right' : 0,
				'margin' : 0,
				'padding' : 0,
				'fontSize' : '480px',
				'cursor' : 'pointer'
			});

			var div = document.createElement("div");
			addStyles(div, {
				'display' : 'block',
				'position' : 'absolute',
				'overflow' : 'hidden',
				'margin' : 0,
				'padding' : 0,
				'opacity' : 0,
				// Make sure browse button is in the right side
				// in Internet Explorer
				'direction' : 'ltr',
				//Max zIndex supported by Opera 9.0-9.2
				'zIndex': 2147483583
			});
			// Make sure that element opacity exists.
			// Otherwise use IE filter
			if ( div.style.opacity !== "0") {
				if (typeof(div.filters) == 'undefined'){
					throw new Error('Opacity not supported by the browser');
				}
				div.style.filter = "alpha(opacity=0)";
			}
			addEvent(input, 'change', function(){
				if ( ! input || input.value === ''){
					return;
				}
				// Get filename from input, required
				// as some browsers have path instead of it
				var file = fileFromPath(input.value);
				if (false === self._settings.onChange.call(self, file, getExt(file))){
					self._clearInput();
					return;
				}
				// Submit form when value is changed
				if (self._settings.autoSubmit) {
					self.submit();
				}
			});

			addEvent(input, 'mouseover', function(){
				addClass(self._button, self._settings.hoverClass);
			});
			addEvent(input, 'mouseout', function(){
				removeClass(self._button, self._settings.hoverClass);
				// We use visibility instead of display to fix problem with Safari 4
				// The problem is that the value of input doesn't change if it
				// has display none when user selects a file
				input.parentNode.style.visibility = 'hidden';

			});
			div.appendChild(input);
			document.body.appendChild(div);
			this._input = input;
		},
		_clearInput : function(){
			if (!this._input){
				return;
			}
			// this._input.value = ''; Doesn't work in IE6
			removeNode(this._input.parentNode);
			this._input = null;
			this._createInput();
			removeClass(this._button, this._settings.hoverClass);
		},
		/**
		 * Function makes sure that when user clicks upload button,
		 * the this._input is clicked instead
		 */
		_rerouteClicks: function(){
			var self = this;
			// IE will later display 'access denied' error
			// if you use using self._input.click()
			// other browsers just ignore click()

			addEvent(self._button, 'mouseover', function(){
				if (self._disabled){
					return;
				}
				if ( ! self._input){
					self._createInput();
				}
				var div = self._input.parentNode;
				copyLayout(self._button, div);
				div.style.visibility = 'visible';
			});
			// commented because we now hide input on mouseleave
			/**
			 * When the window is resized the elements
			 * can be misaligned if button position depends
			 * on window size
			 */
			//addResizeEvent(function(){
			//	if (self._input){
			//		copyLayout(self._button, self._input.parentNode);
			//	}
			//});
		},
		/**
		 * Creates iframe with unique name
		 * @return {Element} iframe
		 */
		_createIframe: function(){
			// We can't use getTime, because it sometimes return
			// same value in safari :(
			var id = getUID();
			// We can't use following code as the name attribute
			// won't be properly registered in IE6, and new window
			// on form submit will open
			// var iframe = document.createElement('iframe');
			// iframe.setAttribute('name', id);

			var iframe = toElement('<iframe src="javascript:false;" name="' + id + '" />');
			// src="javascript:false; was added
			// because it possibly removes ie6 prompt
			// "This page contains both secure and nonsecure items"
			// Anyway, it doesn't do any harm.
			iframe.setAttribute('id', id);
			iframe.style.display = 'none';
			document.body.appendChild(iframe);
			return iframe;
		},
		/**
		 * Creates form, that will be submitted to iframe
		 * @param {Element} iframe Where to submit
		 * @return {Element} form
		 */
		_createForm: function(iframe){
			var settings = this._settings;
			// We can't use the following code in IE6
			// var form = document.createElement('form');
			// form.setAttribute('method', 'post');
			// form.setAttribute('enctype', 'multipart/form-data');
			// Because in this case file won't be attached to request
			var form = toElement('<form method="post" enctype="multipart/form-data"></form>');
			form.setAttribute('action', settings.action);
			form.setAttribute('target', iframe.name);
			form.style.display = 'none';
			document.body.appendChild(form);
			// Create hidden input element for each data key
			for (var prop in settings.data) {
				if (settings.data.hasOwnProperty(prop)){
					var el = document.createElement("input");
					el.setAttribute('type', 'hidden');
					el.setAttribute('name', prop);
					el.setAttribute('value', settings.data[prop]);
					form.appendChild(el);
				}
			}
			return form;
		},
		/**
		 * Gets response from iframe and fires onComplete event when ready
		 * @param iframe
		 * @param file Filename to use in onComplete callback
		 */
		_getResponse : function(iframe, file){
			// getting response
			var toDeleteFlag = false, self = this, settings = this._settings;
			addEvent(iframe, 'load', function(){
				if (// For Safari
					iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" ||
					// For FF, IE
					iframe.src == "javascript:'<html></html>';"){
						// First time around, do not delete.
						// We reload to blank page, so that reloading main page
						// does not re-submit the post.
						if (toDeleteFlag) {
							// Fix busy state in FF3
							setTimeout(function(){
								removeNode(iframe);
							}, 0);
						}
						return;
				}
				var doc = iframe.contentDocument ? iframe.contentDocument : window.frames[iframe.id].document;
				// fixing Opera 9.26,10.00
				if (doc.readyState && doc.readyState != 'complete') {
					// Opera fires load event multiple times
					// Even when the DOM is not ready yet
					// this fix should not affect other browsers
					return;
				}
				// fixing Opera 9.64
				if (doc.body && doc.body.innerHTML == "false") {
					// In Opera 9.64 event was fired second time
					// when body.innerHTML changed from false
					// to server response approx. after 1 sec
					return;
				}
				var response;
				if (doc.XMLDocument) {
					// response is a xml document Internet Explorer property
					response = doc.XMLDocument;
				} else if (doc.body){
					// response is html document or plain text
					response = doc.body.innerHTML;
					if (settings.responseType && settings.responseType.toLowerCase() == 'json') {
						// If the document was sent as 'application/javascript' or
						// 'text/javascript', then the browser wraps the text in a <pre>
						// tag and performs html encoding on the contents.  In this case,
						// we need to pull the original text content from the text node's
						// nodeValue property to retrieve the unmangled content.
						// Note that IE6 only understands text/html
						if (doc.body.firstChild && doc.body.firstChild.nodeName.toUpperCase() == 'PRE') {
							response = doc.body.firstChild.firstChild.nodeValue;
						}
						if (response) {
							response = eval("(" + response + ")");
						} else {
							response = {};
						}
					}
				} else {
					// response is a xml document
					response = doc;
				}
				settings.onComplete.call(self, file, response);
				// Reload blank page, so that reloading main page
				// does not re-submit the post. Also, remember to
				// delete the frame
				toDeleteFlag = true;
				// Fix IE mixed content issue
				iframe.src = "javascript:'<html></html>';";
			});
		},
		/**
		 * Upload file contained in this._input
		 */
		submit: function(){
			var self = this, settings = this._settings;
			if ( ! this._input || this._input.value === ''){
				return;
			}
			var file = fileFromPath(this._input.value);
			// user returned false to cancel upload
			if (false === settings.onSubmit.call(this, file, getExt(file))){
				this._clearInput();
				return;
			}
			// sending request
			var iframe = this._createIframe();
			var form = this._createForm(iframe);
			// assuming following structure
			// div -> input type='file'
			removeNode(this._input.parentNode);
			removeClass(self._button, self._settings.hoverClass);
			form.appendChild(this._input);
			form.submit();

			// request set, clean up
			removeNode(form); form = null;
			removeNode(this._input); this._input = null;
			// Get response from iframe and fire onComplete event when ready
			this._getResponse(iframe, file);

			// get ready for next request
			this._createInput();
		}
	};
})();
